import numpy as np
import pandas as pd
from sklearn.metrics.pairwise import cosine_similarity


def user_base_pearson(arr=[], top_n=2):
    """
    基于用户的最近邻推荐

    alice对物品1-4的评分：[5，3，4，4]
    其它用户对物品1-5的平均：
    [[3, 1, 2, 3, 3],
    [4, 3, 4, 3, 5],
    [3, 3, 1, 5, 4],
    [1, 5, 5, 2, 1]]
    问题：预测alice对物品5的评分

    此例需要注意的地方：
    实际应用中，评分数据集非常大，包含成千上万甚至百万量级的用户和物品，这就要求我们考虑计算复杂度
    此外，评分矩阵通常非常稀疏，每个用户只对所有有效物品中非常小的一个子集评分

    :param arr: 其它用户对物品的打分二维数组
    :param top_n: 用最相似的前n各用户进行预测值平滑
    :return:
    """
    def predict(arr, sim_arr, top_n):
        """
        :param arr: 所有用户的相似度排序数组
        :param top_n: 取前n个相似用户
        :return:
        """
        arr_new = []
        for num, info in enumerate(sim_arr):
            if num + 1 <= top_n:
                mid_arr = arr[info[0]]
                arr_new.append([info[1], np.mean(mid_arr), mid_arr[-1]])
        # 分子
        molecule = sum([info[0] * (info[-1] - info[1]) for info in arr_new])
        # 分母
        Denominator = sum([info[0] for info in arr_new])
        predict_base = np.mean([info[-1] for info in arr_new])
        return predict_base + molecule / Denominator

    # 初始化
    alice = [5, 3, 4, 4]
    arr = [[3, 1, 2, 3, 3],
    [4, 3, 4, 3, 5],
    [3, 3, 1, 5, 4],
    [1, 5, 5, 2, 1]] if len(arr) == 0 else arr

    # 计算用户相似度
    user_sim = dict()
    for i, info in enumerate(arr):
        user_sim.update({i: np.corrcoef(alice, info[: -1])[0][1]})

    user_sim_sort = sorted(user_sim.items(), key=lambda d: d[1], reverse=True)
    """
    如果直接用最相似的用户进行predict，存在如下问题
    1、有些用户倾向于只给高分
    2、有些用户从不给任何物品5分
    
    故，需要用top n进行平滑
    """
    #  预测评分
    pred = predict(arr=arr, sim_arr=user_sim_sort, top_n=top_n)
    print(pred)


def product_base_sim(arr=[]):
    """
    基于物品的最近邻推荐

    前4个物品的评分：
    [[1.00, 0.60, 0.20, -0.20, -1.80],
    [-1.00, -1.40, -0.80, -0.20, 2.20],
    [0.00, -0.40, 0.20, -2.20, 2.20],
    [0.00, 0.60, -0.80, 2.80, -0.80]]
    物品5的评分：[0.60, 1.20, 0.80, -1.80]

    问题：预测alice对物品5的评分

    :param arr:
    :return:
    """
    pred_product = [0.60, 1.20, 0.80, -1.80]
    arr = [[1.00, 0.60, 0.20, -0.20, -1.80],
    [-1.00, -1.40, -0.80, -0.20, 2.20],
    [0.00, -0.40, 0.20, -2.20, 2.20],
    [0.00, 0.60, -0.80, 2.80, -0.80]] if len(arr) == 0 else arr

    # 计算余弦相似度：[目标产品的其他评分，当前产品与目标产品的余弦相似度]
    cos_info = [[info[-1],
                 cosine_similarity(np.array(info[1:]).reshape(1, -1), np.array(pred_product).reshape(1, -1))[0][0]]
                for info in arr]
    # 预测
    pred_score = sum([info[0] * info[1] for info in cos_info]) / sum([info[1] for info in cos_info])
    print(pred_score)


def user_base_svd(arr=[]):
    """
    基于svd的推荐

    评分矩阵：每行为同一物品的不同用户的评分；每列为同一用户对不同物品进行评分
    [[3, 4, 3, 1],
    [1, 3, 2, 6],
    [2, 4, 1, 5],
    [3, 3, 5, 2]]

    alice的评分：[5, 3, 4, 4]

    问题：预测alice的评分

    :param arr: 评分矩阵
    :return:
    """
    # 初始化
    alice = [5, 3, 4, 4]

    arr = [[3, 4, 3, 1],
            [1, 3, 2, 6],
            [2, 4, 1, 5],
            [3, 3, 5, 2]] if len(arr) == 0 else arr

    # svd计算：左奇异向量U：对应物品；右奇异向量V：对应用户；sigma为对角线上的奇异值
    U, sigmal, VT = np.linalg.svd(arr)

    # alice的数据点：alice * U * simga.I
    alice_data = np.dot(np.dot(alice, U),
               np.mat([[sigmal[0], 0, 0, 0], [0, sigmal[1], 0, 0], [0, 0, sigmal[2], 0], [0, 0, 0, sigmal[-1]]]).I)
    print(alice_data)

    #  预测：
    #  alice的近邻用户(VT)：余弦相似度(基本与user_base一致)
    user_sim = [[i, cosine_similarity(np.array(alice_data[0]).reshape(1, -1), np.array(user).reshape(1, -1))[0][0]]
                for i, user in enumerate(VT.T)]
    print(user_sim)

    #  潜在因子空间中用户和物品的相互关系（余弦相似度）,近似计算alice对物品的评分


def bayes_classifer(arr=[]):
    """
    贝叶斯分类器

    alice对物品的评分：[1, 3, 3, 2]
    4个用户对物品的评分：
    [[2， 4， 2， 2， 4],
    [1， 3， 3， 5， 1],
    [4， 5， 2， 3， 3],
    [1， 1， 5， 2， 1]]
    问题：预测alice对第五个物品的评分

    :param arr:
    :return:
    """
    # 初始化
    alice = [1, 3, 3, 2]
    arr = [[2, 4, 2, 2, 4],
    [1, 3, 3, 5, 1],
    [4, 5, 2, 3, 3],
    [1, 1, 5, 2, 1]] if len(arr) == 0 else arr

    # 计算物品5分别为指定评分下的符合alice的条件概率
    # P(X|物品5=i) = P(物品1=1||物品5=i) * P(物品2=3||物品5=i) * P(物品3=3||物品5=i) * P(物品4=2||物品5=i)
    # Pred(物品5 = i)（预测值） = P(X|物品5=i) * P(物品5=i)（现有概率）
    p1_x = 1 * 0.5 * 0.5 * 0.5
    p2_x = 0 * 0 * 0 * 0
    p3_x = 0 * 0 * 0 * 0
    p4_x = 0 * 0 * 0 * 1
    p5_x = 0 * 0 * 0 * 0

    px_1 = 0.5
    px_2 = 0
    px_3 = 0.25
    px_4 = 0.25
    px_5 = 0

    p1 = p1_x * px_1
    p2 = p2_x * px_2
    p3 = p3_x * px_3
    p4 = p4_x * px_4
    p5 = p5_x * px_5

    print(p1, p2, p3, p4, p5)


def slope_one(arr=[]):
    """
    Slope One预测器

    alice对物品的评分：[1, 3, 3, 2]
    5个物品的评分：
    [[2, 1, 4, 1],
    [4, nan, 5, 1],
    [2, 3, 2, 5],
    [2, 5, nan, nan],
    [4, 1, 3, 1]]
    问题：预测alice对第五个物品的评分

    :param arr:
    :return:
    """
    # 初始化
    alice = [1, 3, 3, 2]
    arr = [[2, 1, 4, 1],
           [4, np.nan, 5, 1],
           [2, 3, 2, 5],
           [2, 5, np.nan, np.nan],
           [4, 1, 3, 1]] if len(arr) == 0 else arr

    pred_total = 0
    evalute_total = 0
    #  逐个预测
    for num, info in enumerate(arr[: -1]):
        mid_df = pd.DataFrame([info, arr[-1]])
        #  两两商品共同存在评分
        mid_df = mid_df.dropna(axis=1)
        #  两组(对照组和目标组)评分差的均值
        evaluate = (mid_df.iloc[0] - mid_df.iloc[1]).mean()
        # 通过单组的预测结果
        mid_pred = alice[num] - evaluate
        evalute_total += mid_df.shape[1]
        pred_total += mid_df.shape[1] * mid_pred
        print('num：{}, evaluate: {}, mid_pred: {}, sum: {}'.
              format(num, evaluate, mid_pred, mid_df.shape[1] * mid_pred))
    #  所有物品的平均预测结果
    pred = pred_total / evalute_total
    print(pred)


def run():
    #  基于用户的最近邻推荐：皮尔逊相关系数
    #  user_base_pearson()
    #  基于物品的最近邻推荐：余弦相似度
    #  product_base_sim()
    #  基于SVD的推荐
    #  user_base_svd()
    #  bayes分类器
    #  bayes_classifer()
    # slope_one
    slope_one()


if __name__ == '__main__':
    run()
